iT邦幫忙

2022 iThome 鐵人賽

DAY 19
0

大家晚安~
今天很晚才發文,不是因為我很lazy

而是雙腿剛從馬拉松完賽的狀態恢復
希望對身體沒有什麼副作用(硬要跟本日主題扯在一起XD)

前兩天有提到 lazy evaluationlazy sequences的特性,後來也用lazy sequences實作了 Fibonacci sequence

我們也知道了lazy evaluationcall-by-need
因為需要才call的特性,讓我們在每次 iteration 可以只關心head(初始的步驟),而不理會 rest sequence,等到真正需要算的時候才會計算。這種特性非常的重要!

如果當realization process涉及Side effect(副作用)時,
惰性序列就沒辦法好好地工作。

lazy sequences do not work well when the realization process involves side-effects.

那麼,side effect是什麼呢?

來看懂一個跟side effect有關的例子

我們來舉一個很大的數列(1到100,000,000),並且分成兩組,
一組是1到8,一組是剩下的數(100,000,000-8),
並且用count計算前一組和後一組的個數:

(let [[start end] (split-with #(< % 8) (range 1e8))]
[(count start) (count end)])
[8 99999992]

第一個例子雖然花費了幾秒鐘,但還是算得出來:

(0 1 2 3 4 5 6 7) 有8個
(range 8 100000000) 有 99,999,992個

OOME

第二個例子,我們把 (count end)放在 (count start) 前面

(let [[start end] (split-with #(< % 8) (range 1e8))]
[(count end) (count start)])


;; a few minute later
Execution error (OutOfMemoryError) at java.lang.Long/valueOf (Long.java:840).
GC overhead limit exceeded

出現了傳說中的OOME(Out of Memory Error) 嗎?

(然後我就只好把電腦重開機了XD)

為什麼會這樣呢?
那個split-with究竟在中間隱藏了什麼魔法呢?:)

編按:之所以會舉 1e8的例子,是因為如果數字再小十倍的話 1e7,電腦依舊跑得動XD

探索clojure API: split-with, partial, take-while, drop-while

split-with: 將冰山劈開

以下想以稍微簡單的例子來介紹split-with

(split-with (partial > 5) [1 3 5 7 9])

來看一下文件說明split-with了解到最終會回傳一個vector,只是裡面分成兩組sequence

split-with

Returns a vector of [(take-while pred coll) (drop-while pred coll)]

partial: 作為分組條件

首先看最內層的括弧,
partial先去一刀切,
把collection內符合條件(> 5 element)的predicate items放前面,其餘放後面並且回傳vector

(split-with (partial > 5) [1 3 5 7 9])
[(1 3) (5 7 9)]

(split-with (partial >= 5) [1 3 5 7 9])
[(1 3 5) (7 9)]

partial的好處是可以對對the first argument做某項操作,然後binding到其他的arguments
doc

partial 

(partial f)
(partial f arg1) 
(partial f arg1 arg2)
(partial f arg1 arg2 arg3)
(partial f arg1 arg2 arg3 & more)

Takes a function f and fewer than the normal arguments to f, 
and returns a fn that takes a variable number of additional args. 

When called, the returned function calls f with args + additional args.

假設我們def five-x partial會在每次被call的時候都執行 *,最後再 * 5

(def multiple-five (partial * 5))
#'tutorial.core/multiple-five

(multiple-five 1)
5 ;; 5 * 1

(multiple-five 1 2)
10 ;; 5 * 1 * 2

(multiple-five 1 2 3)
30 ;; 5 * 1 * 2 * 3

(multiple-five 1 2 3 4)
120  ;; 5 * 1 * 2 * 3 * 4

加法也是一樣

(def add-ten (partial + 10))
#'tutorial.core/add-ten

(add-ten 1)
11 ;; (+ 10 1)

(add-ten 1 2)
13 ;; (+ 10 1 2)

(add-ten 1 2 3)
16 ;; (+ 10 1 2 3)
(add-ten 1 2 3 4)
20 ;; (+ 10 1 2 3 4)

如果是把partial用在不等式呢?

(def greater-then (partial > 5))
#'tutorial.core/greater-then

(greater-then 1)
true ;; 要看成 (> 5 1), 結果是true 


(greater-then 3)
true ;; 要看成 (> 5 3), 結果是true 

(greater-then 5)
false ;; 要看成 (> 5 5), 結果是false 

據說partial還可以再跟apply比較 XD

但至少現在對於partial的基本用法很熟悉了
因此我們先打住在此~

其他未知的clojure宇宙就留給看倌們繼續研究囉!

take-while vs. drop-while

第17天的鐵人賽有說到以下這些分類都是Lazy Sequence

;;包括我們最常用的 map 和 filter

map
filter
range
remove

take
take-while

drop
drop-while

剛好本文一開頭?的例子有提到take-while / drop-while,隱藏在split-with的API說明裡

split-with

Returns a vector of [(take-while pred coll) (drop-while pred coll)]

因此走過路過不要錯過,一起來看一些簡單的例子

take-while

(take-while pred)(take-while pred coll)

Returns a lazy sequence of successive items from coll while
(pred item) returns logical true. p

red must be free of side-effects.
Returns a transducer when no collection is provided.

登登愣登!
看到了關鍵字是lazy sequence,
neg?看到最後一個不符合條件false就會停止往下走,

(take-while neg? [-2 -1 0])
=> (-2 -1)

(take-while neg? [-2 -1 0 -1 -2])
=> (-2 -1)


 (take-while neg? [-1 0 -1])
=> (-1)

(take-while neg? [2 1 0 -1 -2])
=> ()

例如 (take-while neg? [-2 -1 0 -1 -2])
即便0後面還有負數,走到0也就停止繼續走下去了
(跟filter會全部檢查不一樣)

是不是有夠lazy呀!

take-while stops traversing the collection when the predicate is false, as is different from filter.

drop-while

想象中drop-while應該是會和take-while剛好反向

讓我們來看看結果是不是如想像中的一樣:

(drop-while neg? [-2 -1 0])
;; 0 
;; result of take-while (-2 -1)

(drop-while neg? [-2 -1 0 -1 -2])
;; (0 -1 -2) 
;; result of take-while (-2 -1)

(drop-while neg? [-1 0 -1])
;; (0 -1) ;; 
;; result of take-while (-1)

(drop-while neg? [2 1 0 -1 -2])
;; (2 1 0 -1 -2) 
;; result of take-while  ()

果然推測沒有錯(推眼鏡?)
只要是take-while保留的,drop-while就會捨去
並回傳剩下的lazy sequence

side effect究竟發生在哪個階段呢?

讓我們回頭來比較開頭的這兩個例子:

(let [[start end] (split-with #(< % 8) (range 1e8))]
[(count start) (count end)])
[8 99999992]
(let [[start end] (split-with #(< % 8) (range 1e8))]
[(count end) (count start)])


;; a few minute later
Execution error (OutOfMemoryError) at java.lang.Long/valueOf (Long.java:840).
GC overhead limit exceeded

在這兩個例子裡,原本的sequence傳進 split-with後,會被記憶體realized因此造成OOME。

當屬於前段start部分的sequence會被直接保留,仍然還是個lazy sequence,是還沒被realized的狀態。

start sequence讓原本的sequence保留的方式如下:
我們想像start sequence是一個一整大塊(thunk)的LazySeq,只有會在被call的時候realized(實現)。

那要怎麼知道start sequence有被call到呢?

這個大塊未知的LazySeq就會需要一個pointer,或者稱做reference

我們再細看一下split-with的說明

split-with

Returns a vector of [(take-while pred coll) (drop-while pred coll)]

並且把原本的

(let [[start end] (split-with #(< % 8) (range 1e8))]
[(count start) (count end)])

換回比較囉唆的超展開版本XD

(let [[start end] 
  [(take-while #(< % 8) (range 1e8)) (drop-while #(< % 8) (range 1e8))]]
  [(count start) (count end)])

當start sequence被realized之後,那一塊儲存的記憶體就不再需要被reference,因此會啟動記憶體的垃圾回收機制(Garbage Collection,GC)

而當end sequence也被call到需要被realized時,

第一個例子沒有太大的問題,是因為當start sequence被evaluated完之後,Clojure compiler會很聰明第知道知道並且reset至nil f(t, t=null)。因此start sequence的值已經被傳給(count start),但原本的sequence就會被清空,這就是傳說中的side effect啦!

第二個例子裡,count end(較多element的LazySeq)的evaluate是在 count start之前,所以記憶體已經被end sequence chuck住,但是start sequnece又必須等end squence elavulate完畢,因此造成Out of memory error。

以上的說明若不清楚,可以看這個clojure group討論串

延伸思考:pure function

pure function就是指將相同的輸入丟入,永遠都會回傳相同的輸出,
並且不會對任何function以外的任何作用域產生影響。除了不但不會受到其他作用域的影響,也不會影響其他作用域的值,也就是沒有Side Effect副作用

我們從今天例子的說明,就可以知道在Clojure有引入了side effect(以及透過reference(atom)對state進行控制),因此它不算是真正的pure function。但Clojure也很強調對side effect的管理。

之後鐵人賽後面幾篇的clojure前端套件介紹也會講到state管理唷!

附註:跟side effect有關的API

do / try 就是一個很好的例子

do

Evaluates the expressions in order and returns the value of the last. 
If no expressions are supplied, returns nil. 

See http://clojure.org/special_forms for more information.

try

The exprs are evaluated and, 
if no exceptions occur, the value of the last is returned. 

If an exception occurs and catch clauses are provided, each is examined in turn and the first for which the thrown exception is an instance of the named class is considered a matching catch clause. 

If there is a matching catch clause, its exprs are evaluated in a context in which name is bound to the thrown exception, and the value of the last is the return value of the function. 

If there is no matching catch clause, the exception
propagates out of the function. Before returning, normally or abnormally,

any finally exprs will be evaluated for their side effects. 

See http://clojure.org/special_forms for more information.

有空大家也可以參考clojure API裡面跟side effect有關的例子,自己試試看:)

Ref


上一篇
[Day18] Clojure Laziness (2) Recursive function & Fibonacci sequence 費波那契數列
下一篇
[Day20] Clojure vs Java / ClojureScript vs Javascript
系列文
後端Developer實戰ClojureScript: Reagent與前端框架 Reframe30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言